package hrc.tool.xml;

import java.io.File;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * the base class that control xml file, you can delete,update,insert or query a
 * xml dom with xpath language in a flexible way
 * 
 * @author hrc
 * 
 */
public abstract class XmlTeller {
	protected Document m_doc;
	protected String fileName;

	public Document getM_doc() {
		return m_doc;
	}

	public NodeList getNodeList(String xpath) {
		try {
			return (NodeList) XPathFactory.newInstance().newXPath().evaluate(
					xpath, this.m_doc, XPathConstants.NODESET);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * update a xml dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language
	 * @param attrName
	 *            the attribute you what to update
	 * @param attrValue
	 *            the new attribute value you what to update
	 * @throws XmlException
	 *             throw it if your xpath string is not match the specification
	 *             of xpath language, or the length of attrName array is not the
	 *             sama as attrValue
	 */
	public void update(String xpath, String[] attrName, String[] attrValue)
			throws XmlException {
		NodeList nodeList = this.getNodeList(xpath);
		this.checkAttributeAvilable(attrName, attrValue);
		this.update(nodeList, attrName, attrValue, null);
	}

	/**
	 * update a xml dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language.however,you
	 *            can replace the value with "?" mark
	 * @param selectValue
	 *            a array that need to be match the ? of xpath param
	 * @param attrName
	 *            the attribute you what to update
	 * @param attrValue
	 *            the new attribute value you what to update
	 * @throws XmlException
	 *             throw it if your xpath string is not match the specification
	 *             of xpath language, or the length of attrName array is not the
	 *             sama as attrValue
	 */
	public void update(String xpath, String[] selectValue, String[] attrName,
			String[] attrValue) throws Exception {
		try {
			NodeList nodeList = this.getNodeList(StringParamSwitcher
					.replaceQuestionMark(xpath, selectValue));

			this.checkAttributeAvilable(attrName, attrValue);
			this.update(nodeList, attrName, attrValue, null);
		} catch (Exception e) {
			throw new XmlException(e);
		}
	}

	/**
	 * update a xml dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language.however,you
	 *            can replace the value with "?" mark
	 * @param selectValue
	 *            a array that need to be match the ? of xpath param
	 * @param attrName
	 *            the attribute you what to update
	 * @param attrValue
	 *            the new attribute value you what to update
	 * @param textContent
	 *            the content of the node you want to update
	 * @throws XmlException
	 *             throw it if your xpath string is not match the specification
	 *             of xpath language, or the length of attrName array is not the
	 *             sama as attrValue
	 */
	public void update(String xpath, String[] selectValue, String[] attrName,
			String[] attrValue, String textContent) throws XmlException {
		try {
			NodeList nodeList = this.getNodeList(StringParamSwitcher
					.replaceQuestionMark(xpath, selectValue));

			this.checkAttributeAvilable(attrName, attrValue);
			this.update(nodeList, attrName, attrValue, textContent);
		} catch (Exception e) {
			
			throw new XmlException(e);
		}
	}

	/**
	 * update a xml dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language
	 * @param attrName
	 *            the attribute you what to update
	 * @param attrValue
	 *            the new attribute value you what to update
	 * @param textContent
	 *            the content of the node you want to update
	 * @throws XmlException
	 *             throw it if your xpath string is not match the specification
	 *             of xpath language, or the length of attrName array is not the
	 *             sama as attrValue
	 */
	public void update(String xpath, String[] attrName, String[] attrValue,
			String textContent) throws XmlException {
		NodeList nodeList = this.getNodeList(xpath);
		this.checkAttributeAvilable(attrName, attrValue);
		this.update(nodeList, attrName, attrValue, textContent);
	}

	/**
	 * get a java bean by query the xml dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language
	 * @param mapper
	 *            your must implements the XmlMapper interface for the instance
	 *            of your java bean
	 * @return java bean object compare with the "xmlToBean" method in
	 *         BeanAndXmlConverter, this mothed allow you to build a more
	 *         flexible bean which mean that you can load the var you need in
	 *         the dom,but you must implement the XmlMapper
	 */
	public <T> T queryForObject(String xpath, XmlMapper<T> mapper) {

		NodeList nodeList = this.getNodeList(xpath);
		return queryForObject(nodeList, mapper);
	}

	/**
	 * get a java bean by query the xml dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language.however,you
	 *            can replace the value with "?" mark
	 * @param selectValue
	 *            a array that need to be match the ? of xpath param
	 * @param mapper
	 *            your must implements the XmlMapper interface for the instance
	 *            of your java bean
	 * @return java bean object
	 * @throws XmlException
	 *             throw it only when your selectValue array is not match the
	 *             "?" in the xpath compare with the "xmlToBean" method in
	 *             BeanAndXmlConverter, this mothed allow you to build a more
	 *             flexible bean which mean that you can load the var you need
	 *             in the dom,but you must implement the XmlMapper
	 */
	public <T> T queryForObject(String xpath, String[] selectValue,
			XmlMapper<T> mapper) throws XmlException {

		try {
			NodeList nodeList = this.getNodeList(StringParamSwitcher
					.replaceQuestionMark(xpath, selectValue));
			return queryForObject(nodeList, mapper);
		} catch (Exception e) {
			throw new XmlException(e);

		}

	}

	/**
	 * get the length of the result
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language.however,you
	 *            can replace the value with "?" mark
	 * @param selectValue
	 *            a array that need to be match the ? of xpath param
	 * @return the length of the result
	 * @throws XmlException
	 *             throw it when your selectValue array is not match the "?" in
	 *             the xpath or your xpath string is wrong
	 */
	public int queryForInt(String xpath, String[] selectValue)
			throws XmlException {
		try {
			NodeList nodeList = this.getNodeList(StringParamSwitcher
					.replaceQuestionMark(xpath, selectValue));
			return nodeList.getLength();
		} catch (Exception e) {
			throw new XmlException(e);

		}
	}

	/**
	 * get the length of the result
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language
	 * @return the result length
	 * @throws XmlException
	 *             throw it when your xpath string is wrong
	 */
	public int queryForInt(String xpath) throws XmlException {
		NodeList nodeList = this.getNodeList(xpath);
		return nodeList.getLength();
	}

	private <T> T queryForObject(NodeList nodeList, XmlMapper<T> mapper) {
		if (nodeList.getLength() == 0) {
			return null;
		}
		return mapper.mapRow((Element) nodeList.item(0), 0);
	}

	/**
	 * get the java bean list of by query the dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language.however,you
	 *            can replace the value with "?" mark
	 * @param selectValue
	 *            a array that need to be match the ? of xpath param
	 * @param mapper
	 *            your must implements the XmlMapper interface for the instance
	 *            of your java bean
	 * @return all java beans load in the list
	 * @throws XmlException
	 *             throw it when your selectValue array is not match the "?" in
	 *             the xpath or your xpath is wrong compare with the "xmlToBean"
	 *             method in BeanAndXmlConverter, this mothed allow you to build
	 *             a more flexible bean which mean that you can load the var you
	 *             need in the dom,but you must implement the XmlMapper
	 */
	public <T> List<T> querys(String xpath, String[] selectValue,
			XmlMapper<T> mapper) throws XmlException {
		try {
			NodeList nodeList = this.getNodeList(StringParamSwitcher
					.replaceQuestionMark(xpath, selectValue));
			return this.querys(nodeList, mapper);
		} catch (Exception e) {
			throw new XmlException(e);
		}

	}

	/**
	 * get the java bean list of by query the dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language
	 * @param mapper
	 *            your must implements the XmlMapper interface for the instance
	 *            of your java bean
	 * @return all java beans load in the list
	 * @throws XmlException
	 *             throw is only when your xpath string is wrong
	 */
	public <T> List<T> querys(String xpath, XmlMapper<T> mapper)
			throws XmlException {

		NodeList nodeList = this.getNodeList(xpath);
		return this.querys(nodeList, mapper);
	}

	private <T> List<T> querys(NodeList nodeList, XmlMapper<T> mapper) {
		List<T> list = new ArrayList<T>();
		if (nodeList.getLength() == 0) {
			return list;
		}
		for (int i = 0; i < nodeList.getLength(); i++) {
			list.add(mapper.mapRow((Element) nodeList.item(i), i));
		}
		return list;
	}

	private void update(NodeList nodeList, String[] attrName,
			String[] attrValue, String textContent) {
		for (int i = 0; i < nodeList.getLength(); i++) {
			Element node = (Element) nodeList.item(i);

			for (int j = 0; j < attrName.length; j++) {
				if (textContent != null) {
					node.setTextContent(textContent);
				}
				node.setAttribute(attrName[j], attrValue[j]);
			}
		}
	}

	/**
	 * insert a new node in the dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language
	 * @param attrName
	 *            the attr of the new node you what to insert
	 * @param attrValue
	 *            attr value
	 * @param nodeName
	 *            the tag name of the new node
	 * @throws XmlException
	 *             throw it only when the length of attrName is not the same as
	 *             the attrValue or your xpath string is wrong
	 */
	public void insert(String xpath, String[] attrName, String[] attrValue,
			String nodeName) throws XmlException {

		NodeList nodeList = this.getNodeList(xpath);
		this.checkAttributeAvilable(attrName, attrValue);
		this.insert(nodeList, attrName, attrValue, nodeName, null);
	}

	/**
	 * insert a new node in the dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language
	 * @param attrName
	 *            the attr of the new node you what to insert
	 * @param attrValue
	 *            attr value
	 * @param nodeName
	 *            the tag name of the new node
	 * @param textContent
	 *            the content text of the new node
	 * @throws XmlException
	 *             throw it only when your xpath string is wrong
	 */
	public void insert(String xpath, String[] attrName, String[] attrValue,
			String nodeName, String textContent) throws XmlException {

		NodeList nodeList = this.getNodeList(xpath);
		this.checkAttributeAvilable(attrName, attrValue);
		this.insert(nodeList, attrName, attrValue, nodeName, textContent);
	}

	private void insert(NodeList nodeList, String[] attrName,
			String[] attrValue, String nodeName, String textContent) {
		Element newNode = m_doc.createElement(nodeName);
		if (textContent != null) {
			newNode.setTextContent(textContent);
		}
		for (int i = 0; i < attrName.length; i++) {
			newNode.setAttribute(attrName[i], attrValue[i]);
		}

		for (int i = 0; i < nodeList.getLength(); i++) {
			Node node = nodeList.item(i);
			node.appendChild(newNode);
		}
	}

	/**
	 * insert a new node in the dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language.however,you
	 *            can replace the value with "?" mark
	 * @param selectValue
	 *            a array that need to be match the ? of xpath param
	 * @param attrName
	 *            the attr of the new node you what to insert
	 * @param attrValue
	 *            attr value
	 * @param nodeName
	 *            the tag name of the new node
	 * @throws XmlException
	 *             throw it only when the length of attrName is not the same as
	 *             the attrValue or your xpath string is wrong
	 */
	public void insert(String xpath, String[] selectValue, String[] attrName,
			String[] attrValue, String nodeName) throws XmlException {

		try {
			NodeList nodeList = this.getNodeList(StringParamSwitcher
					.replaceQuestionMark(xpath, selectValue));
			this.checkAttributeAvilable(attrName, attrValue);
			this.insert(nodeList, attrName, attrValue, nodeName, null);
		} catch (Exception e) {
			throw new XmlException(e);
		}

	}

	/**
	 * insert a new node in the dom
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language.however,you
	 *            can replace the value with "?" marka array that need to be
	 *            match the ? of xpath param
	 * @param attrName
	 *            the attr of the new node you what to insert
	 * @param attrValue
	 *            attr value
	 * @param nodeName
	 *            the tag name of the new node
	 * @param textContent
	 *            the content text of the new node
	 * @throws XmlException
	 *             throw it only when the length of attrName is not the same as
	 *             the attrValue or your xpath string is wrong
	 */
	public void insert(String xpath, String[] selectValue, String[] attrName,
			String[] attrValue, String nodeName, String textContent)
			throws XmlException {

		try {
			NodeList nodeList = this.getNodeList(StringParamSwitcher
					.replaceQuestionMark(xpath, selectValue));
			this.checkAttributeAvilable(attrName, attrValue);
			this.insert(nodeList, attrName, attrValue, nodeName, textContent);
		} catch (Exception e) {
			throw new XmlException(e);
		}

	}

	private void checkAttributeAvilable(String[] attrName, String[] attrValue)
			throws XmlException {
		if (attrName.length != attrValue.length) {
			throw new XmlException(
					"attribute-name's length and attribute-value's length not match");
		}
	}

	private void delectNodeList(NodeList nodeList) {
		for (int i = 0; i < nodeList.getLength(); i++) {
			Node node = nodeList.item(i);
			Node father = node.getParentNode();
			father.removeChild(node);
		}
	}

	/**
	 * delete the dom bt xpath
	 * 
	 * @param xpath
	 *            a string that meet specification of xpath language
	 */
	public void delete(String xpath) {
		NodeList nodeList = this.getNodeList(xpath);
		this.delectNodeList(nodeList);
	}

	/**
	 * delete the dom bt xpath
	 * 
	 * @param xpath
	 *            xpath a string that meet specification of xpath
	 *            language.however,you can replace the value with "?" marka
	 *            array that need to be match the ? of xpath param
	 * @param selectValue
	 *            a array that need to be match the ? of xpath param
	 * @throws XmlException
	 *             throw it only when the length of attrName is not the same as
	 *             the attrValue
	 */
	public void delete(String xpath, String[] selectValue) throws XmlException {

		try {
			NodeList nodeList = this.getNodeList(StringParamSwitcher
					.replaceQuestionMark(xpath, selectValue));
			this.delectNodeList(nodeList);
		} catch (Exception e) {			
			throw new XmlException(e);
		}

	}

	protected void saveXml() throws XmlException {
		try {
			TransformerFactory tFactory = TransformerFactory.newInstance();
			Transformer transformer = tFactory.newTransformer();
			DOMSource source = new DOMSource(m_doc);
			File file = new File(fileName);

			StreamResult result = new StreamResult(file);
			transformer.transform(source, result);

		} catch (Exception e) {
			throw new XmlException(e);
		}
	}

	/**
	 * tranfer the xml dom to string
	 */
	public String toString() {
		StringWriter output = new StringWriter();
		try {
			TransformerFactory.newInstance().newTransformer().transform(
					new DOMSource(m_doc), new StreamResult(output));
		} catch (Exception e) {
		}
		return output.toString();
	}

	/**
	 * don't forget to user is method after you deal with a dom
	 * 
	 * @throws XmlException
	 */
	public abstract void save() throws XmlException;

	/**
	 * save the dom to other file
	 * 
	 * @param fileName
	 *            the name of the new file
	 * @throws XmlException
	 *             throw it when create file fail
	 */
	public void save(String fileName) throws XmlException {
		try {
			TransformerFactory tFactory = TransformerFactory.newInstance();
			Transformer transformer = tFactory.newTransformer();
			DOMSource source = new DOMSource(m_doc);
			File file = new File(fileName);

			StreamResult result = new StreamResult(file);
			transformer.transform(source, result);

		} catch (Exception e) {
			throw new XmlException(e);
		}
	}

	static class StringParamSwitcher {
		public static String replaceQuestionMark(String str, String[] params)
				throws Exception {
			StringBuffer buffer = new StringBuffer();
			Pattern pattern = Pattern.compile("\\?");
			Matcher matcher = pattern.matcher(str);
			int i = 0;
			while (matcher.find()) {
				matcher.appendReplacement(buffer, params[i]);
				i++;
			}
			if (params.length != i) {
				throw new Exception(
						"\"?\" and paramters length is not match");
			}
			matcher.appendTail(buffer);
			return buffer.toString();
		}
	}
}
